/**
 * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information regarding copyright ownership. Apereo
 * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License. You may obtain a copy of the License at the
 * following location:
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0
 *
 * <p>Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apereo.portal.services;

import java.util.Iterator;
import javax.naming.InvalidNameException;
import javax.naming.Name;
import org.apereo.portal.EntityIdentifier;
import org.apereo.portal.concurrency.CachingException;
import org.apereo.portal.groups.CompositeServiceIdentifier;
import org.apereo.portal.groups.GroupServiceConfiguration;
import org.apereo.portal.groups.GroupsException;
import org.apereo.portal.groups.ICompositeGroupService;
import org.apereo.portal.groups.ICompositeGroupServiceFactory;
import org.apereo.portal.groups.IEntity;
import org.apereo.portal.groups.IEntityGroup;
import org.apereo.portal.groups.IGroupConstants;
import org.apereo.portal.groups.IGroupMember;
import org.apereo.portal.groups.ILockableEntityGroup;
import org.apereo.portal.properties.PropertiesManager;
import org.apereo.portal.security.IPerson;
import org.apereo.portal.utils.threading.SingletonDoubleCheckedCreator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Bootstrap class for the IGroupService implementation. */
public final class GroupService implements IGroupConstants {

    private static final String GROUP_SERVICE_KEY = "org.apereo.portal.services.GroupService.key_";
    private static final Logger LOGGER = LoggerFactory.getLogger(GroupService.class);

    // Singleton instance of the bootstrap class:
    private static final SingletonDoubleCheckedCreator<GroupService> instance =
            new SingletonDoubleCheckedCreator<GroupService>() {
                @Override
                protected GroupService createSingleton(Object... args) {
                    return new GroupService();
                }
            };

    // The group service:
    private ICompositeGroupService compositeGroupService = null;

    /** Creates new GroupService */
    private GroupService() throws GroupsException {
        super();
        initializeCompositeService();
    }

    /**
     * Returns the groups that contain the <code>IGroupMember</code>.
     *
     * @param gm IGroupMember
     */
    public static Iterator findParentGroups(IGroupMember gm) throws GroupsException {
        LOGGER.trace("Invoking findParentGroups for IGroupMember [{}]", gm);
        return instance().ifindParentGroups(gm);
    }

    /**
     * Returns a pre-existing <code>IEntityGroup</code> or null if the <code>IGroupMember</code>
     * does not exist.
     *
     * @param key String - the group key.
     * @return org.apereo.portal.groups.IEntityGroup
     */
    public static IEntityGroup findGroup(String key) throws GroupsException {
        LOGGER.trace("Invoking findGroup for key='{}'", key);
        return instance().ifindGroup(key);
    }

    /**
     * Returns a pre-existing <code>ILockableEntityGroup</code> or null if the group is not found.
     *
     * @param key String - the group key.
     * @param lockOwner String - the owner of the lock, typically the user.
     * @return org.apereo.portal.groups.ILockableEntityGroup
     */
    public static ILockableEntityGroup findLockableGroup(String key, String lockOwner)
            throws GroupsException {
        LOGGER.trace("Invoking findLockableGroup for key='{}', lockOwner='{}'", key, lockOwner);
        return instance().ifindLockableGroup(key, lockOwner);
    }

    /**
     * Receives notice that the UserInstance has been unbound from the HttpSession. In response, we
     * remove the corresponding group member from the cache.
     *
     * @param person org.apereo.portal.security.IPerson
     */
    public static void finishedSession(IPerson person) {
        LOGGER.trace("Invoking finishedSession for IPerson [{}]", person);
        try {
            instance().ifinishedSession(person);
        } catch (GroupsException ge) {
            LOGGER.error("Error upon session finishing for person [{}]", person, ge);
        }
    }

    /**
     * Returns the <code>ICompositeGroupService</code> implementation in use.
     *
     * @return org.apereo.portal.groups.ICompositeGroupService
     */
    public static ICompositeGroupService getCompositeGroupService() throws GroupsException {
        LOGGER.trace("Invoking getCompositeGroupService()");
        return instance().compositeGroupService;
    }

    /** @return java.lang.String */
    private String getDefaultServiceName() throws GroupsException {
        return (String) getServiceConfiguration().getAttributes().get("defaultService");
    }
    /**
     * Refers to the PropertiesManager to get the key for the group associated with 'name' and asks
     * the group store implementation for the corresponding <code>IEntityGroup</code>.
     */
    public static IEntityGroup getDistinguishedGroup(String name) throws GroupsException {
        LOGGER.trace("Invoking getDistinguishedGroup for name='{}'", name);
        return instance().igetDistinguishedGroup(name);
    }

    /** @return java.lang.String */
    public String getDistinguishedGroupKey(String name) {
        return PropertiesManager.getProperty(GROUP_SERVICE_KEY + name, "");
    }

    /**
     * Returns an <code>IEntity</code> representing a portal entity. This does not guarantee that
     * the entity actually exists.
     *
     * @param key String - the group key.
     * @param type Class - the Class of the underlying IGroupMember.
     * @return org.apereo.portal.groups.IEntity
     */
    public static IEntity getEntity(String key, Class<?> type) throws GroupsException {
        return getEntity(key, type, null);
    }

    /**
     * Returns an <code>IEntity</code> representing a portal entity. This does not guarantee that
     * the entity actually exists.
     *
     * @param key String - the group key.
     * @param type Class - the Class of the underlying IGroupMember.
     * @param service String - the name of the component service.
     * @return org.apereo.portal.groups.IEntity
     */
    private static IEntity getEntity(String key, Class<?> type, String service)
            throws GroupsException {
        return instance().igetEntity(key, type, service);
    }

    /**
     * Returns an <code> IGroupMember </code> representing either a group or a portal entity. If the
     * parm <code> type </code> is the group type, the <code> IGroupMember </code> is an <code>
     *  IEntityGroup </code> else it is an <code> IEntity </code> .
     */
    public static IGroupMember getGroupMember(String key, Class<?> type) throws GroupsException {
        /*
         * WARNING:  The 'type' parameter is not the leafType;  you're obligated
         * to say whether you want a group or a non-group (i.e. some type of
         * entity).  In fact, the underlying implementation blindly instantiates
         * whatever you tell it to.
         */

        LOGGER.trace("Invoking getEntity for key='{}', type='{}'", key, type);
        return instance().igetGroupMember(key, type);
    }

    /**
     * Returns an <code>IGroupMember</code> representing either a group or a portal entity, based on
     * the <code>EntityIdentifier</code>, which refers to the UNDERLYING entity for the <code>
     * IGroupMember</code>.
     */
    public static IGroupMember getGroupMember(EntityIdentifier underlyingEntityIdentifier)
            throws GroupsException {
        return getGroupMember(
                underlyingEntityIdentifier.getKey(), underlyingEntityIdentifier.getType());
    }

    /**
     * Refers to the PropertiesManager to get the key for the root group associated with 'type' and
     * asks the group store implementation for the corresponding <code>IEntityGroup</code>.
     */
    public static IEntityGroup getRootGroup(Class<?> type) throws GroupsException {
        LOGGER.trace("Invoking getRootGroup for type='{}'", type);
        return instance().igetRootGroup(type);
    }

    /** @return java.lang.String */
    private GroupServiceConfiguration getServiceConfiguration() throws GroupsException {
        try {
            return GroupServiceConfiguration.getConfiguration();
        } catch (Exception ex) {
            throw new GroupsException("Problem retrieving service configuration", ex);
        }
    }

    /**
     * Returns the groups that contain the <code>IGroupMember</code>.
     *
     * @param gm IGroupMember
     */
    private Iterator ifindParentGroups(IGroupMember gm) throws GroupsException {
        return compositeGroupService.findParentGroups(gm);
    }

    /**
     * Returns a pre-existing <code>IEntityGroup</code> or null if the <code>IGroupMember</code>
     * does not exist.
     *
     * @param key String - the group key.
     * @return org.apereo.portal.groups.IEntityGroup
     */
    private IEntityGroup ifindGroup(String key) throws GroupsException {
        return compositeGroupService.findGroup(key);
    }

    /**
     * Returns a pre-existing <code>ILockableEntityGroup</code> or null if the group is not found.
     *
     * @param key String - the group key.
     * @param lockOwner String - typically the user.
     * @return org.apereo.portal.groups.ILockableEntityGroup
     */
    private ILockableEntityGroup ifindLockableGroup(String key, String lockOwner)
            throws GroupsException {
        return compositeGroupService.findGroupWithLock(key, lockOwner);
    }

    /**
     * Receives notice that the UserInstance has been unbound from the HttpSession. In response, we
     * remove the corresponding group member from the cache. We use the roundabout route of creating
     * a group member and then getting its EntityIdentifier because we need the EntityIdentifier for
     * the group member, which is cached, not the EntityIdentifier for the IPerson, which is not.
     *
     * @param person org.apereo.portal.security.IPerson
     */
    private void ifinishedSession(IPerson person) throws GroupsException {
        IGroupMember gm = getGroupMember(person.getEntityIdentifier());
        try {
            final EntityIdentifier entityIdentifier = gm.getEntityIdentifier();
            EntityCachingService.getEntityCachingService()
                    .remove(entityIdentifier.getType(), entityIdentifier.getKey());
        } catch (CachingException ce) {
            throw new GroupsException(
                    "Problem removing group member " + gm.getKey() + " from cache", ce);
        }
    }

    /**
     * Refers to the PropertiesManager to get the key for the group associated with 'name' and asks
     * the group store implementation for the corresponding <code>IEntityGroup</code>.
     */
    private IEntityGroup igetDistinguishedGroup(String name) throws GroupsException {
        try {
            String key = getDistinguishedGroupKey(name);
            return compositeGroupService.findGroup(key);
        } catch (Exception ex) {
            throw new GroupsException(
                    "GroupService.getDistinguishedGroup(): " + "could not find key for: " + name,
                    ex);
        }
    }

    /**
     * Returns an <code>IEntity</code> representing a pre-existing portal entity.
     *
     * @param key String - the group key.
     * @param type Class - the Class of the underlying IGroupMember.
     * @return org.apereo.portal.groups.IEntity
     */
    private IEntity igetEntity(String key, Class<?> type, String service) throws GroupsException {
        return compositeGroupService.getEntity(key, type, service);
    }

    /**
     * Returns an <code>IGroupMember</code> representing either a group or a portal entity. If the
     * parm <code>type</code> is the group type, the <code>IGroupMember</code> is an <code>
     * IEntityGroup</code> else it is an <code>IEntity</code>.
     */
    private IGroupMember igetGroupMember(String key, Class<?> type) throws GroupsException {
        return compositeGroupService.getGroupMember(key, type);
    }

    /**
     * Refers to the PropertiesManager to get the key for the root group associated with 'type' and
     * asks the group store implementation for the corresponding <code>IEntityGroup</code>.
     */
    private IEntityGroup igetRootGroup(Class<?> type) throws GroupsException {
        return igetDistinguishedGroup(type.getName());
    }
    /**
     * Returns a new <code>IEntityGroup</code> for the given Class with an unused key.
     *
     * @return org.apereo.portal.groups.IEntityGroup
     */
    private IEntityGroup inewGroup(Class<?> type) throws GroupsException {
        return inewGroup(type, getDefaultServiceName());
    }
    /**
     * Returns a new <code>IEntityGroup</code> for the given Class with an unused key.
     *
     * @return org.apereo.portal.groups.IEntityGroup
     */
    private IEntityGroup inewGroup(Class<?> type, String serviceName) throws GroupsException {
        try {
            return compositeGroupService.newGroup(type, parseServiceName(serviceName));
        } catch (InvalidNameException ine) {
            throw new GroupsException("GroupService.inewGroup(): invalid service name", ine);
        }
    }

    /** @exception GroupsException */
    private void initializeCompositeService() throws GroupsException {
        String eMsg = null;
        try {
            GroupServiceConfiguration cfg = getServiceConfiguration();
            String factoryName = (String) cfg.getAttributes().get("compositeFactory");

            if (factoryName == null) {
                eMsg =
                        "GroupService.initialize(): No entry for CompositeServiceFactory in configuration";
                LOGGER.error(eMsg);
                throw new GroupsException(eMsg);
            }

            ICompositeGroupServiceFactory serviceFactory =
                    (ICompositeGroupServiceFactory) Class.forName(factoryName).newInstance();
            compositeGroupService = serviceFactory.newGroupService();
        } catch (Exception e) {
            eMsg =
                    "GroupService.initialize(): Problem creating groups service... "
                            + e.getMessage();
            LOGGER.error(eMsg, e);
            throw new GroupsException(eMsg, e);
        }
    }

    private static GroupService instance() throws GroupsException {
        return instance.get();
    }

    /**
     * Returns a new <code>IEntityGroup</code> for the given Class with an unused key.
     *
     * @return org.apereo.portal.groups.IEntityGroup
     */
    public static IEntityGroup newGroup(Class<?> type) throws GroupsException {
        LOGGER.trace("Invoking newGroup for type='{}'", type);
        return instance().inewGroup(type);
    }

    /**
     * Converts the String form of a service name into a Name.
     *
     * @return javax.naming.Name
     * @exception InvalidNameException
     * @exception GroupsException
     */
    public static Name parseServiceName(String serviceName)
            throws InvalidNameException, GroupsException {
        return new CompositeServiceIdentifier(serviceName).getServiceName();
    }

    public static EntityIdentifier[] searchForEntities(
            String query, SearchMethod method, Class<?> type) throws GroupsException {
        LOGGER.trace(
                "Invoking searchForEntities for query='{}', method='{}', type='{}'",
                query,
                method,
                type);
        return instance().compositeGroupService.searchForEntities(query, method, type);
    }

    public static EntityIdentifier[] searchForEntities(
            String query, SearchMethod method, Class<?> type, IEntityGroup ancestor)
            throws GroupsException {
        LOGGER.trace(
                "Invoking searchForEntities for query='{}', method='{}', type='{}', ancestor='{}'",
                query,
                method,
                type,
                ancestor);
        return instance().compositeGroupService.searchForEntities(query, method, type, ancestor);
    }

    public static EntityIdentifier[] searchForGroups(
            String query, SearchMethod method, Class<?> leaftype) throws GroupsException {
        LOGGER.trace(
                "Invoking searchForGroups for query='{}', method='{}', leaftype='{}'",
                query,
                method,
                leaftype);
        return instance().compositeGroupService.searchForGroups(query, method, leaftype);
    }

    public static EntityIdentifier[] searchForGroups(
            String query, SearchMethod method, Class<?> leaftype, IEntityGroup ancestor)
            throws GroupsException {
        LOGGER.trace(
                "Invoking searchForGroups for query='{}', method='{}', leaftype='{}', ancestor='{}'",
                query,
                method,
                leaftype,
                ancestor);
        return instance().compositeGroupService.searchForGroups(query, method, leaftype, ancestor);
    }
}
